# 剑指Offer题解 - Day49
# 剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
示例 1:
输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
2
限制:
2 <= nums.length <= 10000
思路:
根据题目要求,我们首先得出几个结论:
- 数组元素都是整型。
- 只有两个数字出现了一次。
- 要求时间复杂度是
O(n)
,空间复杂度是O(1)
。
由于要求空间复杂度为O(1)
,因此不能使用额外的数据结构存储数组元素的出现次数。
# 位运算
本题需要使用位运算来题解。位运算不需要开辟额外的空间,可以让空间复杂度降低至O(1)
。
我们要利用 异或运算 的一个特性:两个相同数字异或为 0。并且异或运算满足交换律。意味着运算结果与数字顺序无关。
如果说,只有一个数字出现了一次,那么我们可以这样写:
const singleNumber = (nums) => {
let r = 0;
for (const num of nums) {
r ^= num;
}
return r;
}
2
3
4
5
6
7
初始化一个结果变量并赋值为0,因为0和任意值异或都等于任意值。然后遍历数组并不断的进行异或运算,最终返回的值就是只出现一次的数字。
但是本题是有两个数字出现了一次,因此不能直接使用此方法进行运算。
假设两个出现一次的数字为x
和y
。如果说,我们能将x和y分别放至两个子数组中,并且满足每个数组只有x和y出现了一次,其余数字出现两次。那么就可以直接使用上述方法进行题解。
/**
* @param {number[]} nums
* @return {number[]}
*/
var singleNumbers = function(nums) {
let x = 0; // 初始化结果数字x
let y = 0; // 初始化结果数字y
let z = 0; // 初始化x ^ y的结果
let r = 1; // 初始化获取z首位1的变量
for (const num of nums) {
z ^= num; // z最终为x ^ y
}
while(!(z & r)) {
r <<= 1; // r最终为z首位为1的值
}
for (const num of nums) {
if ((num & r) !== 0) x ^= num; // 根据r将x和y拆分到不同子数组中
else y ^= num;
}
return [x, y];
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 时间复杂度 O(n)。
- 空间复杂度 O(1)。
分析:
首先我们通过遍历数组获取x ^ y
的值,记为z
。然后我们通过不断左移初始值为1
的r
,并让r
和z
执行 与操作 。
如果发现z & r
不为0
,意味着r
找到了x ^ y
最右侧值为1
的位。那么这个为1的位意味着什么?由异或运算的特点可知,异或结果为1
,意味着当前位肯定是不同的。也就是说,最终r
的值代表了x
和y
在指定位上值不相同。
那用r
来干什么呢?我们可以再一次遍历数组,将数组的元素
和r
进行 与运算 ,由于x
和y
在指定位上是不同的,因此可以完美的将x
和y
区分开来。
那么会有人问了,我们将x
和y
拆分到两个子数组中,怎么确保子数组中刚好只有x
和y
只出现一次呢?这是因为数组的每个元素和r
进行 与运算 ,如果两个数字是相等的,那么指定位上的值肯定是相同的,这样拆分肯定将相同的值拆分到同一个子数组当中。
最极端的情况就是,所有出现两次的数字的指定位都相同,那最终拆分出来的子数组就是一个数组只有x
或者y
,另一个有剩余所有元素。
这样拆分,就回到文章最初分析的只有一个元素出现一次的情况。接下来只需要将x
和y
不断和拆分的数组元素 异或运算 即可。
最终返回由x
和y
组成的数组。
# 总结
本题考查位运算中异或运算的特性。分别分析了一个元素出现一次和两个元素出现一次的情况。在明天的题解中,会继续分析一个元素出现一次,其他元素出现三次的情况,继续学起来吧。